package licode.unisop.oidc.oauth2.service;

import licode.unisop.oidc.auth.api.OidcSession;
import licode.unisop.oidc.auth.conf.AuthorDef;
import licode.unisop.oidc.auth.conf.AuthorMode;
import licode.unisop.oidc.auth.modal.*;
import licode.unisop.oidc.auth.stu.AuthorUtil;
import licode.unisop.oidc.auth.utils.StringTool;
import licode.unisop.oidc.auth.utils.UrlTool;
import licode.unisop.oidc.oauth2.utils.CodeUtil;
import licode.unisop.provider.api.SiLocker;
import licode.unisop.provider.api.SiOauthProvider;
import licode.unisop.provider.info.*;
import licode.unisop.provider.utils.StrUtil;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * Oauth2授权实现
 * <p>
 * 授权码模式的步骤如下：
 * <p>
 * 1）请求oauth/authorize获取code
 * 2）认证通过之后，重定向到redirect_uri，code的值在地址上
 * 3）获取到code，请求/oauth/token，grant_type=authorization_code
 * 4）认证通过之后，获取到token信息
 *
 * @author licode
 */
@Service
public class SiOauthAuthorizationImpl implements SiOauthProvider {
    @Resource
    private SiLocker siLocker;

    private AuthorRequest convertAuthContextIn(SiAuthContextIn contextIn) {
        AuthorRequest request = getAuthorRequest(contextIn);
        request.setState(contextIn.getState());
        request.getContext().setUid(contextIn.getUserId());
        request.getContext().setAc(contextIn.getAccount());
        request.getContext().setPip(contextIn.getPeerIp());
        request.getContext().setAt(contextIn.getAuthType());
        request.getContext().setRc(contextIn.getRoleCodes());
        request.getContext().setRn(contextIn.getRoleNames());
        request.getContext().setLt(contextIn.getUserType());
        request.getContext().setRid(contextIn.getRealmId());
        request.getContext().setPid(contextIn.getPlatformId());
        request.getContext().setTer(contextIn.getTerminal());
        request.getContext().setCid(contextIn.getClientId());
        request.getContext().setTid(contextIn.getTenantId());
        request.getContext().setAe(contextIn.getAccessExpired());
        request.getContext().setRe(contextIn.getRefreshExpired());
        return request;
    }

    private static AuthorRequest getAuthorRequest(SiAuthContextIn contextIn) {
        AuthorRequest request = new AuthorRequest();
        request.setSsoToken("");
        request.setClientId(contextIn.getClientId());
        request.setClientSecret(contextIn.getClientSecret());
        request.setScope(contextIn.getScope());
        request.setCode("");
        request.setRedirectUri(contextIn.getTicketUri());
        request.setGrantType(contextIn.getGrantType());
        request.setState(contextIn.getState());
        request.setAuthMode(contextIn.getAuthMode());
        request.setContext(new AuthorContext());
        return request;
    }

    private Map<String, Object> oidcTokenToMap(OidcToken token) {
        if (null == token) {
            return null;
        }

        OidcContext context = token.getContext();
        Map<String, Object> retMap = new HashMap<>();

        retMap.put("realmId", context.getRealmId());
        retMap.put("clientId", context.getClientId());
        retMap.put("platformId", context.getPlatformId());
        retMap.put("tenantId", context.getTenantId());
        retMap.put("terminalId", context.getTerminal());
        retMap.put("userId", context.getUserId());
        retMap.put("account", context.getAccount());
        retMap.put("authType", context.getAuthType());
        retMap.put("roleCodes", context.getRoleCodes());
        retMap.put("roleNames", context.getRoleNames());
        retMap.put("peerIp", context.getPeerIp());
        retMap.put("userType", context.getLoginType());
        retMap.put("accessToken", token.getAccessToken());
        retMap.put("refreshToken", token.getRefreshToken());
        retMap.put("accessExpired", token.getAccessExpiresIn());
        retMap.put("refreshExpired", token.getRefreshExpiresIn());

        return retMap;
    }

    @Override
    public SiCodeInfo buildCode(SiAuthContextIn request) {
        AuthorRequest authReq = convertAuthContextIn(request);

        if (AuthorMode.SSO.equals(request.getAuthMode())) {
            // 创建主会话
            SsoToken ssoToken;
            SiLocker.Locker locker = siLocker.getLocker(authReq.getContext().calcLoginId());
            try {
                locker.lock(5000);
                ssoToken = AuthorUtil.buildMasterSession(authReq);
            } finally {
                locker.unlock();
            }

            return buildCodeInfo(request, ssoToken.getSsoToken());
        } else {
            return buildCodeInfo(request, "");
        }
    }

    @Override
    public SiCodeInfo autoSso(SiAuthContextIn auth) {
        OidcSession session;
        session = AuthorUtil.getSession(SsoTokenId.build(auth.getSsoToken()));
        if (null == session || session.getSsoLoginCount() == 0) {
            // 会话没有创建或没有客户端登录，直接返回
            return null;
        } else {
            auth.setUserId(session.getUserId());
            return fillAutoSsoUserInfo(auth, session, auth.getSsoToken());
        }
    }

    public SiCodeInfo fillAutoSsoUserInfo(SiAuthContextIn auth, OidcSession session, String ssoToken) {
        OidcToken oidcToken = session.getFirstSsoTokenInfo(ssoToken);
        if (null != oidcToken) {
            auth.setAccount(oidcToken.getContext().getAccount());
            auth.setAuthMode(AuthorMode.SSO);
            auth.setAuthType(oidcToken.getContext().getAuthType());
            auth.setRoleCodes(oidcToken.getContext().getRoleCodes());
            auth.setRoleNames(oidcToken.getContext().getRoleNames());

            if (auth.getAccessExpired() <= 0) {
                auth.setAccessExpired(oidcToken.getAccessExpiresIn());
            }
            if (auth.getRefreshExpired() <= 0) {
                auth.setRefreshExpired(oidcToken.getRefreshExpiresIn());
            }

            return buildCodeInfo(auth, auth.getSsoToken());
        } else {
            return null;
        }
    }

    public SiCodeInfo buildCodeInfo(SiAuthContextIn request, String ssoToken) {
        SiCodeInfo codeInfo = new SiCodeInfo();
        AuthorRequest authReq = convertAuthContextIn(request);
        AuthorCode code = buildAuthCode(authReq);
        AuthorUtil.getCache().setObject(splicingCodeSaveKey(code.getCode()),
                authReq, 300);

        codeInfo.setCode(code.getCode());
        codeInfo.setSsoToken(ssoToken);

        return codeInfo;
    }

    @Override
    public Map<String, Object> buildCredential(SiAuthCode codeInfo) {
        Object codeVal;
        String codeKey = splicingCodeSaveKey(codeInfo.getCode());
        codeVal = AuthorUtil.getCache().getObject(codeKey);

        if (null != codeVal) {
            AuthorRequest authReq = (AuthorRequest) codeVal;
            AuthorUtil.getCache().delete(codeKey);

            if (null != authReq.getState() && !authReq.getState().isEmpty() &&
                    !authReq.getState().equals(codeInfo.getState())) {
                return null;
            }
            SiLocker.Locker locker = siLocker.getLocker(authReq.getContext().calcLoginId());
            try {
                locker.lock(5000);
                if (AuthorMode.SSO.equals(authReq.getAuthMode()) ||
                        AuthorMode.SSO.equals(codeInfo.getAuthMode())) {
                    return oidcTokenToMap(AuthorUtil.loginSso(authReq));
                } else {
                    return oidcTokenToMap(AuthorUtil.login(authReq));
                }
            } finally {
                locker.unlock();
            }
        } else {
            return null;
        }
    }

    @Override
    public Map<String, Object> buildCredential(SiAuthContextIn request) {
        AuthorRequest authReq = convertAuthContextIn(request);
        SiLocker.Locker locker = siLocker.getLocker(authReq.getContext().calcLoginId());
        try {
            locker.lock(5000);
            return oidcTokenToMap(AuthorUtil.login(authReq));
        } finally {
            locker.unlock();
        }
    }

    @Override
    public Map<String, Object> getCredential(TokenIdInfo idInfo) {
        OidcToken oidcToken = AuthorUtil.getTokenInfo(AccessTokenId.build(idInfo.getToken()));

        if (null != oidcToken) {
            boolean needRefresh = false;

            if (oidcToken.getLastRenewalTime() == null) {
                needRefresh = true;
            } else {
                long renewTime = System.currentTimeMillis() / 1000 - oidcToken.getLastRenewalTime();
                // 如果上一次续约超过120秒，则刷新Token时间
                if (renewTime > 120) {
                    needRefresh = true;
                }
            }

            if (needRefresh) {
                RefreshTokenId refreshTokenId;
                refreshTokenId = RefreshTokenId.build(oidcToken.getRefreshToken())
                        .realmId(oidcToken.getContext().getRealmId());
                AuthorUtil.refresh(refreshTokenId, false);
            }
        }

        return oidcTokenToMap(oidcToken);
    }

    @Override
    public void logoutByUserId(UserIdInfo idInfo) {
        String loginId = idInfo.buildLoginId();
        SiLocker.Locker locker = siLocker.getLocker(loginId);
        try {
            locker.lock(5000);
            AuthorUtil.exitSession(LoginId.build(loginId));
        } finally {
            locker.unlock();
        }
    }

    @Override
    public void logoutByAccessToken(TokenIdInfo idInfo) {
        AccessTokenId tokenId;
        tokenId = AccessTokenId.build(idInfo.getToken()).realmId(idInfo.getRealmId());
        String loginId = AuthorUtil.loginIdFromAccessToken(tokenId);

        if (null != loginId) {
            SiLocker.Locker locker = siLocker.getLocker(loginId);
            try {
                locker.lock(5000);
                AuthorUtil.exitLogin(tokenId);
            } finally {
                locker.unlock();
            }
        }
    }

    @Override
    public void logoutBySso(TokenIdInfo idInfo) {
        AccessTokenId tokenId;
        tokenId = AccessTokenId.build(idInfo.getToken()).realmId(idInfo.getRealmId());
        String loginId = AuthorUtil.loginIdFromAccessToken(tokenId);
        if (null != loginId) {
            SiLocker.Locker locker = siLocker.getLocker(loginId);
            try {
                locker.lock(5000);
                AuthorUtil.exitSso(tokenId);
            } finally {
                locker.unlock();
            }
        }
    }

    @Override
    public void logoutBySession(TokenIdInfo idInfo) {
        AccessTokenId tokenId;
        tokenId = AccessTokenId.build(idInfo.getToken()).realmId(idInfo.getRealmId());
        String loginId = AuthorUtil.loginIdFromAccessToken(tokenId);
        if (null != loginId) {
            SiLocker.Locker locker = siLocker.getLocker(loginId);
            try {
                locker.lock(5000);
                AuthorUtil.exitSession(tokenId);
            } finally {
                locker.unlock();
            }
        }
    }

    @Override
    public Map<String, Object> refresh(TokenIdInfo idInfo, boolean newToken) {
        RefreshTokenId tokenId;
        tokenId = RefreshTokenId.build(idInfo.getToken()).realmId(idInfo.getRealmId());
        String loginId = AuthorUtil.loginIdFromRefreshToken(tokenId);

        if (null != loginId) {
            SiLocker.Locker locker = siLocker.getLocker(loginId);
            try {
                locker.lock(5000);
                return oidcTokenToMap(AuthorUtil.refresh(tokenId, newToken));
            } finally {
                locker.unlock();
            }
        } else {
            return null;
        }
    }

    @Override
    public boolean isLogin(TokenIdInfo idInfo) {
        return StrUtil.isNotBlank(AuthorUtil.loginIdFromAccessToken(AccessTokenId
                .build(idInfo.getToken())
                .realmId(idInfo.getRealmId())));
    }

    private AuthorCode buildAuthCode(AuthorRequest request) {
        AuthorCode authorCode = new AuthorCode(request.getState());

        authorCode.setCode(CodeUtil.getRandomCode());
        authorCode.setState(request.getState());

        return authorCode;
    }

    public String splicingCodeSaveKey(String code) {
        return "oidc:oauth2:code:" + code;
    }

    /**
     * 构建URL：下放Code URL (Authorization Code 授权码)
     *
     * @param redirectUri 下放地址
     * @param code        code参数
     * @param state       state参数
     * @return 构建完毕的URL
     */
    public String buildRedirectUri(String redirectUri, String code, String state) {
        String url = UrlTool.joinParam(redirectUri, AuthorDef.CODE, code);
        if (!StringTool.isEmpty(state)) {
            url = UrlTool.joinParam(url, AuthorDef.STATE, state);
        }
        return url;
    }

    /**
     * 构建URL：下放Access-Token URL （implicit 隐藏式）
     *
     * @param redirectUri 下放地址
     * @param token       token
     * @param state       state参数
     * @return 构建完毕的URL
     */
    public String buildImplicitRedirectUri(String redirectUri, String token, String state) {
        String url = UrlTool.joinSharpParam(redirectUri, AuthorDef.TOKEN, token);
        if (!StringTool.isEmpty(state)) {
            url = UrlTool.joinSharpParam(url, AuthorDef.STATE, state);
        }
        return url;
    }
}
